/**
* This class lets you encode animated GIF files
* Base class :  http://www.java2s.com/Code/Java/2D-Graphics-GUI/AnimatedGifEncoder.htm
* @author Kevin Weiner (original Java version - kweiner@fmsware.com)
* @author Thibault Imbert (AS3 version - bytearray.org)
* @version 0.1 AS3 implementation
*/

package org.bytearray.gif.encoder
{
    import flash.utils.ByteArray;
    import flash.display.BitmapData;
    import flash.display.Bitmap;
    import org.bytearray.gif.encoder.NeuQuant
    import flash.net.URLRequestHeader;
    import flash.net.URLRequestMethod;
    import flash.net.URLRequest;
    import flash.net.navigateToURL;

    public class GIFEncoder
    {
        private var width:int // image size
        private var height:int;
        private var transparent:* = null; // transparent color if given
        private var transIndex:int; // transparent index in color table
        private var repeat:int = -1; // no repeat
        private var delay:int = 0; // frame delay (hundredths)
        private var started:Boolean = false; // ready to output frames
        public var out:ByteArray;
        private var image:Bitmap; // current frame
        private var pixels:ByteArray; // BGR byte array from frame
        private var indexedPixels:ByteArray // converted frame indexed to palette
        private var colorDepth:int; // number of bit planes
        private var colorTab:ByteArray; // RGB palette
        private var usedEntry:Array = new Array; // active palette entries
        private var palSize:int = 7; // color table size (bits-1)
        private var dispose:int = -1; // disposal code (-1 = use default)
        private var closeStream:Boolean = false; // close stream when finished
        private var firstFrame:Boolean = true;
        private var sizeSet:Boolean = false; // if false, get size from first frame
        private var sample:int = 10; // default sample interval for quantizer

        /**
        * Sets the delay time between each frame, or changes it for subsequent frames
        * (applies to last frame added)
        * int delay time in milliseconds
        * @param ms
        */

        public function setDelay(ms:int):void
        {

            delay = Math.round(ms / 10);

        }

        /**
        * Sets the GIF frame disposal code for the last added frame and any
        *
        * subsequent frames. Default is 0 if no transparent color has been set,
        * otherwise 2.
        * @param code
        * int disposal code.
        */

        public function setDispose(code:int):void 
        {

            if (code >= 0) dispose = code;

        }

        /**
        * Sets the number of times the set of GIF frames should be played. Default is
        * 1; 0 means play indefinitely. Must be invoked before the first image is
        * added.
        *
        * @param iter
        * int number of iterations.
        * @return
        */

        public function setRepeat(iter:int):void 
        {

            if (iter >= 0) repeat = iter;

        }

        /**
        * Sets the transparent color for the last added frame and any subsequent
        * frames. Since all colors are subject to modification in the quantization
        * process, the color in the final palette for each frame closest to the given
        * color becomes the transparent color for that frame. May be set to null to
        * indicate no transparent color.
        * @param
        * Color to be treated as transparent on display.
        */

        public function setTransparent(c:Number):void
        {

            transparent = c;

        }

        /**
        * The addFrame method takes an incoming BitmapData object to create each frames
        * @param
        * BitmapData object to be treated as a GIF's frame
        */

        public function addFrame(im:BitmapData):Boolean
        {

            if ((im == null) || !started || out == null) 

            {
                throw new Error ("Please call start method before calling addFrame");
                return false;
            }

            var ok:Boolean = true;

            try {

                image = new Bitmap ( im );
                if (!sizeSet) setSize(image.width, image.height);
                getImagePixels(); // convert to correct format if necessary
                analyzePixels(); // build color table & map pixels

                if (firstFrame) 
                {
                    writeLSD(); // logical screen descriptior
                    writePalette(); // global color table
                    if (repeat >= 0) 
                    {
                        // use NS app extension to indicate reps
                        writeNetscapeExt();
                    }
                }

                writeGraphicCtrlExt(); // write graphic control extension
                writeImageDesc(); // image descriptor
                if (!firstFrame) writePalette(); // local color table
                writePixels(); // encode and write pixel data
                firstFrame = false;
            } catch (e:Error) {
                ok = false;
            }

            return ok;

        }

        /**
        * Adds final trailer to the GIF stream, if you don't call the finish method
        * the GIF stream will not be valid.
        */

        public function finish():Boolean
        {
            if (!started) return false;
            var ok:Boolean = true;
            started = false;
            try {
                out.writeByte(0x3b); // gif trailer
            } catch (e:Error) {
                ok = false;
            }

            return ok;

        }

        /**
        * Resets some members so that a new stream can be started.
        * This method is actually called by the start method
        */

        private function reset ( ):void
        {

            // reset for subsequent use
            transIndex = 0;
            image = null;
            pixels = null;
            indexedPixels = null;
            colorTab = null;
            closeStream = false;
            firstFrame = true;

        }

        /**
        * * Sets frame rate in frames per second. Equivalent to
        * <code>setDelay(1000/fps)</code>.
        * @param fps
        * float frame rate (frames per second)
        */

        public function setFrameRate(fps:Number):void 
        {

            if (fps != 0xf) delay = Math.round(100/fps);

        }

        /**
        * Sets quality of color quantization (conversion of images to the maximum 256
        * colors allowed by the GIF specification). Lower values (minimum = 1)
        * produce better colors, but slow processing significantly. 10 is the
        * default, and produces good color mapping at reasonable speeds. Values
        * greater than 20 do not yield significant improvements in speed.
        * @param quality
        * int greater than 0.
        * @return
        */

        public function setQuality(quality:int):void
        {

            if (quality < 1) quality = 1;
            sample = quality;

        }

        /**
        * Sets the GIF frame size. The default size is the size of the first frame
        * added if this method is not invoked.
        * @param w
        * int frame width.
        * @param h
        * int frame width.
        */

        private function setSize(w:int, h:int):void
        {

            if (started && !firstFrame) return;
            width = w;
            height = h;
            if (width < 1)width = 320;
            if (height < 1)height = 240;
            sizeSet = true

        }

        /**
        * Initiates GIF file creation on the given stream.
        * @param os
        * OutputStream on which GIF images are written.
        * @return false if initial write failed.
        *
        */

        public function start():Boolean
        {

            reset(); 
            var ok:Boolean = true;
            closeStream = false;
            out = new ByteArray;
            try {
                out.writeUTFBytes("GIF89a"); // header
            } catch (e:Error) {
                ok = false;
            }

            return started = ok;

        }

        /**
        * Analyzes image colors and creates color map.
        */

        private function analyzePixels():void
        {

            var len:int = pixels.length;
            var nPix:int = len / 3;
            indexedPixels = new ByteArray;
            var nq:NeuQuant = new NeuQuant(pixels, len, sample);
            // initialize quantizer
            colorTab = nq.process(); // create reduced palette
            // map image pixels to new palette
            var k:int = 0;
            for (var j:int = 0; j < nPix; j++) {
                var index:int = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
                usedEntry[index] = true;
                indexedPixels[j] = index;
            }
            pixels = null;
            colorDepth = 8;
            palSize = 7;
            // get closest match to transparent color if specified
            if (transparent != null) {
                transIndex = findClosest(transparent);
            }
        }

        /**
        * Returns index of palette color closest to c
        *
        */

        private function findClosest(c:Number):int
        {

            if (colorTab == null) return -1;
            var r:int = (c & 0xFF0000) >> 16;
            var g:int = (c & 0x00FF00) >> 8;
            var b:int = (c & 0x0000FF);
            var minpos:int = 0;
            var dmin:int = 256 * 256 * 256;
            var len:int = colorTab.length;

            for (var i:int = 0; i < len;) {
                var dr:int = r - (colorTab[i++] & 0xff);
                var dg:int = g - (colorTab[i++] & 0xff);
                var db:int = b - (colorTab[i] & 0xff);
                var d:int = dr * dr + dg * dg + db * db;
                var index:int = i / 3;
                if (usedEntry[index] && (d < dmin)) {
                    dmin = d;
                    minpos = index;
                }
                i++;
            }
            return minpos;

        }

        /**
        * Extracts image pixels into byte array "pixels
        */

        private function getImagePixels():void
        {

            var w:int = image.width;
            var h:int = image.height;
            pixels = new ByteArray;

            var count:int = 0;

            for ( var i:int = 0; i < h; i++ )
            {

                for (var j:int = 0; j < w; j++ )
                {

                    var pixel:Number = image.bitmapData.getPixel( j, i );

                    pixels[count] = (pixel & 0xFF0000) >> 16;
                    count++;
                    pixels[count] = (pixel & 0x00FF00) >> 8;
                    count++;
                    pixels[count] = (pixel & 0x0000FF);
                    count++;

                }

            }

        }

        /**
        * Writes Graphic Control Extension
        */

        private function writeGraphicCtrlExt():void
        {

            out.writeByte(0x21); // extension introducer
            out.writeByte(0xf9); // GCE label
            out.writeByte(4); // data block size
            var transp:int
            var disp:int;
            if (transparent == null) {
                transp = 0;
                disp = 0; // dispose = no action
            } else {
                transp = 1;
                disp = 2; // force clear if using transparent color
            }
            if (dispose >= 0) {
                disp = dispose & 7; // user override
            }
            disp <<= 2;
            // packed fields
            out.writeByte(0 | // 1:3 reserved
                          disp | // 4:6 disposal
                          0 | // 7 user input - 0 = none
                          transp); // 8 transparency flag

            WriteShort(delay); // delay x 1/100 sec
            out.writeByte(transIndex); // transparent color index
            out.writeByte(0); // block terminator

        }

        /**
        * Writes Image Descriptor
        */

        private function writeImageDesc():void
        {

            out.writeByte(0x2c); // image separator
            WriteShort(0); // image position x,y = 0,0
            WriteShort(0);
            WriteShort(width); // image size
            WriteShort(height);

            // packed fields
            if (firstFrame) {
                // no LCT - GCT is used for first (or only) frame
                out.writeByte(0);
            } else {
                // specify normal LCT
                out.writeByte(0x80 | // 1 local color table 1=yes
                              0 | // 2 interlace - 0=no
                              0 | // 3 sorted - 0=no
                              0 | // 4-5 reserved
                              palSize); // 6-8 size of color table
            }
        }

        /**
        * Writes Logical Screen Descriptor
        */

        private function writeLSD():void
        {

            // logical screen size
            WriteShort(width);
            WriteShort(height);
            // packed fields
            out.writeByte((0x80 | // 1 : global color table flag = 1 (gct used)
                          0x70 | // 2-4 : color resolution = 7
                          0x00 | // 5 : gct sort flag = 0
                          palSize)); // 6-8 : gct size

            out.writeByte(0); // background color index
            out.writeByte(0); // pixel aspect ratio - assume 1:1

        }

        /**
        * Writes Netscape application extension to define repeat count.
        */

        private function writeNetscapeExt():void
        {

            out.writeByte(0x21); // extension introducer
            out.writeByte(0xff); // app extension label
            out.writeByte(11); // block size
            out.writeUTFBytes("NETSCAPE" + "2.0"); // app id + auth code
            out.writeByte(3); // sub-block size
            out.writeByte(1); // loop sub-block id
            WriteShort(repeat); // loop count (extra iterations, 0=repeat forever)
            out.writeByte(0); // block terminator

        }

        /**
        * Writes color table
        */

        private function writePalette():void
        {

            out.writeBytes(colorTab, 0, colorTab.length);
            var n:int = (3 * 256) - colorTab.length;
            for (var i:int = 0; i < n; i++) out.writeByte(0);

        }

        private function WriteShort (pValue:int):void
        {  	

            out.writeByte( pValue & 0xFF );
            out.writeByte( (pValue >> 8) & 0xFF);

        }

        /**
        * Encodes and writes pixel data
        */

        private function writePixels():void
        {

            var myencoder:LZWEncoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
            myencoder.encode(out);

        }

        /**
        * retrieves the GIF stream
        */
        public function get stream ( ):ByteArray
        {

            return out; 

        }

    }
}

